S08-01 Node-基础
[TOC]
概述
Atwood 定律
Atwood 定律:任何可以使用 JavaScript 来实现的应用都最终都会使用 JavaScript 实现。
Stack Overflow 的创立者之一的 Jeff Atwood 在 2007 年提出了著名的 Atwood 定律:
- Any application that can be written in JavaScript, will eventually be written in JavaScript.
- 任何可以使用 JavaScript 来实现的应用都最终都会使用 JavaScript 实现。
但是在发明之初,JavaScript 的目的是应用于在浏览器执行简单的脚本任务,对浏览器以及其中的 DOM 进行各种操作,所以 JavaScript 的应用场景非常受限。
- Atwood 定律更像是一种美好的远景,在当时看来还没有实现的可能性。
- 但是随着 Node 的出现,Atwood 定律已经越来越多的被证实是正确的。
但是为了可以理解 Node.js 到底是如何帮助我们做到这一点的,我们必须了解 JavaScript 是如何被运行的。
浏览器内核
浏览器内核:又名排版引擎(layout engine),也称为浏览器引擎(browser engine)、页面渲染引擎(rendering engine)或样版引擎。
浏览器内核-分类:
- Gecko:早期被 Netscape 和 Mozilla Firefox 浏览器使用;
- Trident:微软开发,被 IE4~IE11 浏览器使用,但是 Edge 浏览器已经转向 Blink;
- Webkit:苹果基于 KHTML 开发、开源的,用于 Safari,Google Chrome 之前也在使用;
- Blink:是 Webkit 的一个分支,Google 开发,目前应用于 Google Chrome、Edge、Opera 等;
浏览器内核-组成:
- WebCore:负责 HTML 解析、布局、渲染等等相关的工作;
- JavaScriptCore:解析、执行 JavaScript 代码;
小程序中编写的 JavaScript 代码就是被 JSCore 执行的
原理图:
但是在这个执行过程中,HTML 解析的时候遇到了 JavaScript 标签,应该怎么办呢?
- 会停止解析 HTML,而去加载和执行 JavaScript 代码;
当然,为什么不直接异步去加载执行 JavaScript 代码,而要在这里停止掉呢?
- 这是因为 JavaScript 代码可以操作我们的 DOM;
- 所以浏览器希望将 HTML 解析的 DOM 和 JavaScript 操作之后的 DOM 放到一起来生成最终的 DOM 树,而不是频繁的去生成新的 DOM 树;
那么,JavaScript 代码由谁来执行呢?
- JavaScript 引擎
JS 引擎
JS 引擎作用:JS 引擎帮助我们将 JS 代码翻译成 CPU 指令来执行。
事实上我们编写的 JavaScript 无论你交给浏览器或者 Node 执行,最后都是需要被 CPU 执行的。但是 CPU 只认识自己的指令集,实际上是机器语言。所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行。
常见 JS 引擎:
- SpiderMonkey:第一款 JavaScript 引擎,由 Brendan Eich 开发(也就是 JavaScript 作者);
- Chakra:微软开发,用于 IE 浏览器;
- JavaScriptCore:WebKit 中的 JavaScript 引擎,Apple 公司开发;
- V8:Google 开发的强大 JavaScript 引擎,也帮助 Chrome 从众多浏览器中脱颖而出;
下面来详细介绍一下 V8 引擎。
V8
V8:是用 C ++编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于Chrome和Node.js等。它实现 ECMAScript 和 WebAssembly,并在 Windows 7 或更高版本,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上运行。V8 可以独立运行,也可以嵌入到任何 C ++应用程序中。
V8 原理图:
V8 引擎本身的源码非常复杂,大概有超过 100w 行 C++代码,但是我们可以简单了解一下它执行 JavaScript 代码的原理:
Parse模块会将 JS 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JavaScript 代码;
注意:如果函数没有被调用,那么是不会被转换成 AST 的;
Parse 的 V8 官方文档:https://v8.dev/blog/scanner
Ignition是一个解释器,会将 AST 转换成 ByteCode(字节码),同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
- 注意:如果函数只调用一次,Ignition 会执行解释执行 ByteCode;
- Ignition 的 V8 官方文档:https://v8.dev/blog/ignition-interpreter
TurboFan是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码;
注意 :如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过 TurboFan 转换成优化的机器码,提高代码的执行性能;
但是,机器码实际上也会被还原为 ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如 sum 函数原来执行的是 number 类型,后来执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
TurboFan 的 V8 官方文档:https://v8.dev/blog/turbofan-jit
Orinoco 事实上 V8 的内存回收也是其强大的另外一个原因,这里暂时先不展开讨论:
- Orinoco 模块,负责垃圾回收,将程序中不需要的内存回收;
- Orinoco 的 V8 官方文档:https://v8.dev/blog/trash-talk
Node 基础
概述
Node.js:是一个基于 V8 JavaScript 引擎的 JavaScript 运行时环境。
浏览器、Node 组成:
浏览器组成:
V8 引擎:运行 JS
DOM:解析渲染 HTML、CSS
BOM:浏览器对象模型(浏览器 API)
Node 组成:
V8 引擎
文件系统读/写
网络 IO
加密
压缩解压文件
Node 架构图:
- 我们编写的 JavaScript 代码会经过 V8 引擎,再通过 Node.js 的 Bindings,将任务放到 Libuv 的事件循环中;
- libuv(Unicorn Velociraptor—独角伶盗龙)是使用 C 语言编写的库;
- libuv 提供了事件循环、文件系统读写、网络 IO、线程池等等内容;
- 具体内部代码的执行流程,我会在后续专门讲解事件和异步 IO 的原理中详细讲解;
Node 应用场景:
- 包管理工具
- npm
- yarn
- pnpm
- 脚手架
- webpack
- vite
- gulp
- 服务器
- web 服务器
- 代理服务器
- 中间件
- SSR
- 脚本工具
- 工程自动化
- Electron
- vscode
依赖安装
依赖包: Node.js
版本:
- LTS:稳定版
- Current:最新版
安装: 和在 windows 上安装其他软件一样
常用命令:
node --version
:查看当前的版本node <path/to/index.js>
:运行 index.js 文件
基本使用
Node 的基本使用:
编写 JS 代码:
在
abc.js
中编写 JS 代码jsconsole.log('a') console.log('b') console.log('c')
终端运行:
通过
node <path>
命令执行 JS 文件
版本管理工具
n
介绍: Linux 环境的 Node 版本管理工具
安装: npm i n -g
常用命令:
n --version
,查看安装的版本n lts
,安装最新的 LTS 版本n latest
,安装最新的版本n
,查看所有的版本
权限获取:
sodu
:Linux 环境中可以通过添加 sudo 前缀,获取执行命令时所需要的权限。
nvm
介绍:Linux 环境的 Node 版本管理工具
nvm-window
介绍:Windows 环境的 Node 版本管理工具
常用命令:
查看 node
nvm list
,查看所有安装的版本nvm list installed
,显示所有已安装的版本nvm list available
,显示所有可以下载的版本
安装 node
nvm install 16.9.0
,安装 16.9.0 版本nvm install 16
,安装 16 大版本的最新小版本nvm install lts
,安装最新的 LTS 版本nvm install latest
,安装最新的版本(不是current
)
卸载 node
nvm uninstall <版本号>
,卸载指定版本的 node
使用指定版本的 node
nvm use <版本号>
,使用指定版本的 node
查看 nvm 版本
nvm version
,查看 nvm 版本nvm --version
,查看 nvm 版本
终端
常用终端:
- CMD
- PowerShell
- Git Bash
VSCode 设置默认终端:
打开 VSCode 终端,按如下顺序点击:
选择默认的终端:
Node 的输入、输出
输入:
1、在终端中运行以下命令:node <index.js> num1=100 num2=200
2、在 js 中通过process.argv
获取终端中输入的参数num1
、num2
输出:
console.log('hello')
,打印消息console.clear()
,清空控制台console.trace()
,打印执行调用栈
REPL
REPL(Read-Eval-Print Loop,“读取-求值-输出”循环):是一个简单的、交互式的编程环境。
常用命令:
- 开启 REPL:
node + 回车
- 退出 REPL:
Ctrl + C(按2次)
.exit
- 清空控制台:
- CMD:
cls
- Git Bash:
Ctrl + L
- Linux:
clear
- CMD:
示例:在 REPL 中的基本操作
全局对象
环境变量
process.env
process.env:当前进程的环境变量。
process.argv
process.argv:一个包含命令行参数的数组。当在命令行中执行 Node.js 脚本时,可以使用该数组来访问传递给脚本的参数。
参数: 接受如下命令行中传递的参数:node <index.js> arg1=xxx arg2=xxx
返回值: 一个包含命令行参数的数组:
- 第一个元素:Node.js 的可执行文件路径
- 第二个元素:被执行的 JavaScript 文件的路径
- 后续元素:命令行传递的参数
示例:process.argv
类似 window
window
window:浏览器环境下的全局对象,Node 环境下没有
注意: 通过 var 定义的变量,会被放入到 window 对象上
global
global:Node 环境下的全局对象,浏览器环境下没有
注意: 通过 var 定义的变量,并不会被放入到 global 对象上
示例:global 对象
globalThis
globalThis:ES2020,是一个跨平台的解决方案。浏览器和 Node 环境下分别指向全局对象 window 和 global
注意:
globalThis === global
,Node 环境中二者等价globalThis === window
,浏览器环境中二者等价
模块
注意: 以下实际上是模块中的变量
require()
require():(id)
,用于加载和引用其他 JS 文件或模块。
id:
string
,要加载的模块名称或路径。返回:
module:
any
,已加载模块的对象。
示例:基本使用
const path = require('path')
exports
说明: 包含模块导出内容的空对象。通过向 exports
对象添加属性或方法,可以将它们导出给其他模块使用。
语法:
exports.xxx = value
exports.xxx = function() { ... }
exports.xxx = () => { ... }
module
说明: 表示当前模块的对象。每个 JS 文件都是一个独立的模块,可以通过 module
对象来访问和控制模块的行为。
语法:
module.exports = {
xxx: value
}
属性:
- module.exports:导出模块内容给其他模块使用。
- module.id:表示当前模块的标识符,通常是文件的绝对路径。
- module.filename:表示当前模块的文件名,通常是文件的绝对路径。
- module.loaded:一个布尔值,表示当前模块是否已经加载完成。
- module.parent:表示当前模块的父级模块。
- module.children:表示当前模块依赖的子模块列表。
方法:
- module.require(id):类似于全局的
require()
函数,用于加载和返回指定模块。
__dirname
说明: 当前文件所在目录(绝对路径)
语法:
// /home/user/projects/myapp/app.js
console.log(__dirname) // /home/user/projects/myapp
__filename
说明: 当前文件所在目录+文件名称(绝对路径)
语法:
// /home/user/projects/myapp/app.js
console.log(__filename) // /home/user/projects/myapp/app.js
URL
URLSearchParams
说明: 处理 URL 查询字符串的接口
语法:
const params = new URLSearchParams(init?)
参数:
- init?:
string|object|URLSearchParams
,被解析的目标。string
:它会被解析为查询参数,并用于初始化URLSearchParams
对象。object
:它会被解析为键值对,并用于初始化URLSearchParams
对象。URLSearchParams
:它会被复制到新的URLSearchParams
对象。
返回值:
- params:
URLSearchParams
,可以使用URLSearchParams
的方法来操作查询参数。
实例方法:
- params.append(name, value):向查询参数中添加一个新的键值对。
- params.delete(name):从查询参数中删除指定名称的键值对。
- params.get(name):获取查询参数中指定名称的第一个值。
- params.getAll(name):获取查询参数中指定名称的所有值的数组。
- params.has(name):检查查询参数中是否存在指定名称的键值对。
- params.set(name, value):将查询参数中指定名称的键值对设置为新的值。
- params.sort():按照名称对查询参数进行排序。
- params.toString():返回表示查询参数的字符串。
示例:解析查询字符串
var baseUrl = 'http://example.com/search?query=tom&age=33'
const url = new URL(baseUrl)
// 获取查询字符串
const queryString = url.search // ?query=tom&age=33
// 方式一:获取 URLSearchParams 对象
const query = url.searchParams // URLSearchParams { 'query' => 'tom', 'age' => '33' }
// 方式二:获取 URLSearchParams 对象
const params = new URLSearchParams(query) // URLSearchParams { 'query' => 'tom', 'age' => '33' }
// 转化URLSearchParams为对象格式
console.log(Object.fromEntries(params)) // { query: 'tom', age: '33' }
定时器
- window.setTimeout():
(callback,delay?,...args?)
,用于在指定延迟时间后执行函数或代码的全局方法。 - window.setInterval():
(callback,delay,...args?)
,用于周期性无限循环调用函数或代码直到被显式取消的全局方法。 - setImmediate():
(callback,...args?)
,Node,用于安排回调函数在当前事件循环轮次的 "检查" 阶段立即执行的方法。 - window.clearTimeout():
(timeoutID)
,用于取消由setTimeout()
创建的定时器的全局方法。可以阻止尚未执行的定时器回调函数的运行。 - window.clearInterval():
(intervalID)
,用于终止由setInterval()
创建的周期性定时器的全局方法。 - clearImmediate():
(immediateID)
,Node,用于取消由setImmediate()
创建的即将执行的回调函数的方法。
事件循环
定时器
queueMicrotask()
queueMicrotask():(callback)
,Node,浏览器,用于将回调函数加入到微任务队列的全局函数。微任务会在当前 JS 执行栈清空后、事件循环继续之前执行。
- callback:
()=>void
,要在微任务队列中执行的回调函数。该函数不接受任何参数。
示例:
基本用法:
jsqueueMicrotask(() => { console.log('微任务执行'); });
使用闭包传递数据:
jslet data = '重要数据'; queueMicrotask(() => { console.log(`处理数据: ${data}`); });
使用函数声明:
jsfunction microtaskHandler() { console.log('这是一个微任务'); } queueMicrotask(microtaskHandler);
在严格模式下的 this 行为
js'use strict'; queueMicrotask(function() { console.log(this); // 输出: undefined });
核心特性:
微任务执行时机:
微任务在 JS 事件循环中的位置如下:
同步代码执行
→nextTick队列
→微任务队列
→事件循环下一阶段
jsconsole.log('脚本开始'); setTimeout(() => { console.log('setTimeout - 宏任务'); }, 0); queueMicrotask(() => { console.log('queueMicrotask - 微任务'); }); process.nextTick(() => { console.log('process.nextTick - 最高优先级'); }); Promise.resolve().then(() => { console.log('Promise.then - 微任务'); }); console.log('脚本结束'); // 输出顺序: // 脚本开始 // 脚本结束 // process.nextTick - 最高优先级 // queueMicrotask - 微任务 // Promise.then - 微任务 // setTimeout - 宏任务
对比
process.nextTick()
:process.nextTick()
: Node.js 特有,优先级最高。queueMicrotask()
: 浏览器和 Node.js 通用,优先级次之。
对比
Promise.then()
:queueMicrotask()
可以看作是Promise.resolve().then()
的语法糖,但更加直观和高效。js// 以下两种方式是等价的: queueMicrotask(() => { console.log('使用 queueMicrotask'); }); Promise.resolve().then(() => { console.log('使用 Promise.resolve().then()'); });
注意事项:
无法取消:
queueMicrotask()
没有返回标识符,因此无法取消已经排队的微任务,一旦调用,微任务必定会在当前执行栈结束后执行。
process.nextTick()
process.nextTick():(callback, ...args?)
,Node,用于将回调函数延迟到当前宏任务的末尾、微任务继续之前执行的函数。
callback:
Function
,当前执行栈结束后要执行的函数。...args?:
any
,调用回调时要传递的可选参数。
示例:
基本用法:
js// 1. 不带参数 process.nextTick(() => { console.log('在下一个Tick执行'); });
js// 2. 带一个参数 process.nextTick((name) => { console.log(`Hello, ${name}!`); }, 'Alice');
js// 3. 带多个参数 process.nextTick((a, b, c) => { console.log(`计算结果: ${a + b * c}`); }, 2, 3, 4);
js// 4. 使用箭头函数和普通函数 process.nextTick(function() { console.log('这是一个普通函数'); });
核心特性:
执行时机:
process.nextTick()
不属于事件循环的任何阶段(定时器、I/O、检查等),它有一个独立的 "nextTick 队列"。在事件循环的每个阶段之间,Node.js 会检查 nextTick 队列,并执行其中的所有回调:
当前JS代码执行
→nextTick队列
→微任务队列
→事件循环下一阶段
jsconsole.log('开始'); setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }); process.nextTick(() => { console.log('nextTick'); }); console.log('结束'); // 输出顺序: // 开始 // 结束 // nextTick // setTimeout 或 setImmediate(顺序不确定) // setImmediate 或 setTimeout(顺序不确定)
对比
setImmediate()
:process.nextTick()
的优先级 高于setImmediate()
。process.nextTick()
:- 不属于事件循环的任何阶段,它有一个独立的 "nextTick 队列"。
- 会在事件循环当前阶段结束后、进入下一个阶段之前立即执行。
setImmediate()
:- 会在 事件循环的 "检查" 阶段 执行。
jssetImmediate(() => { console.log('setImmediate'); }); process.nextTick(() => { console.log('nextTick'); }); console.log('当前执行栈'); // 输出顺序(确定性的): // 当前执行栈 // nextTick // setImmediate
对比
Promise.then()
:执行优先级:
process.nextTick()
>Promise.then()
jsconsole.log('开始'); process.nextTick(() => { console.log('nextTick'); }); Promise.resolve().then(() => { console.log('Promise'); }); console.log('结束'); // 输出顺序: // 开始 // 结束 // nextTick // Promise
注意事项:
无法取消:
process.nextTick()
没有返回标识符,因此无法取消已经安排的回调,一旦安排,回调必定会在当前执行栈结束后执行。